feat(scripts): gate repository.directory against the manifest's own directory - #16446
Merged
os-warren merged 2 commits intoSep 7, 2026
Merged
Conversation
… directory npm renders `repository.directory` as a package page's "source" deep link, so a value naming a directory this repo does not have publishes a 404 on the one link a consumer follows to read the code. Nothing in this repo read the field: measured over the tracked tree, zero reads of `.directory` in any `.mjs`/`.mts`, and `check-published-files.mjs` — which has exactly the right population and already parses every manifest — mentions `repository` not once. The notch is a category rather than an off-by-one: this repo gated published package CONTENTS and did not gate published package PROVENANCE METADATA. The new gate is a state check over every tracked manifest that DECLARES the field: the value must be well-formed, must name a directory that is IN the repository (read off `git ls-files`, because the deep link is served from the repository and not from the author's working tree), and must be the manifest's own directory. Stateless over the population on purpose — one of the three commits behind the known residue moved nothing at all, it edited `repository.url` in the same JSON object with the stale `directory` line as visible hunk context, so a rule that fired only on directory MOVES would have missed it exactly as the reviewers did. ⛔ It judges only manifests that declare the field. Whether declaring is mandatory for a publishable package is an open policy question and a maintainer's to answer, so silence is counted, listed and never a finding. Anti-vacuity, because this population is `git ls-files` plus a field predicate and empties silently from either side: two control probes drive the same tracked-directory predicate the invariants use in BOTH directions (a directory derived from the population must report EXISTS, an assembled impossible one must report MISSING) and no verdict prints unless both fire; MEASURED pins the census on a named commit with floors under it; and the `--self-test` carries a battery roster whose names and per-battery case floors red when a battery stops registering cases. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y
This was referenced Sep 7, 2026
…nifest-repository-directory-gate
os-warren
marked this pull request as ready for review
September 7, 2026 02:38
os-warren
enabled auto-merge
September 7, 2026 02:38
os-warren
deleted the
claude/issue-15991-manifest-repository-directory-gate
branch
September 7, 2026 03:35
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #15991 — deliberately not a closing relation. This implements one half of that card; the policy question it raises is untouched and must stay open.
What this adds
scripts/check-manifest-repository-directory.mjs, wired ascheck:manifest-repository-directoryand run as a step inLint & Repo Gatesbeside the published-files whitelist guard.npm renders
repository.directoryas a package page's "source" deep link, so a value naming a directory this repo does not have publishes a 404 on the one link a consumer follows to read the code. Nothing here read the field. Re-derived on this branch rather than taken from the card: zero reads of.directoryin any tracked.mjs/.mts, andcheck-published-files.mjs— which has exactly the right population and already parses every manifest — mentionsrepositorynot once. Its six invariants are all about thefileswhitelist, which is what the tarball CONTAINS. The notch is a category, not an off-by-one: this repo gated published package CONTENTS and did not gate published package PROVENANCE METADATA.Per manifest that DECLARES the field:
..segment, no empty segment, no surrounding whitespace.git ls-filesrather thanexistsSync. The deep link is served by GitHub from the repository, so a gitignored or build directory that exists on the author's disk is exactly the case that 404s for the consumer; reading it from git also makes the question case-exact on a case-insensitive filesystem.⛔ The policy half is deliberately NOT implemented
The card asks whether declaring
repository.directoryis mandatory for a publishable package or whether silence is allowed. That is a maintainer's call — 14 publishable packages declare nothing, and each of those npm pages has NO source link rather than a broken one. This PR implements the consistency check over whoever opts in, and nothing else: a manifest that declares nothing is counted, listed by--list, and is never a finding. The gate's header and its failure text both say so, and a self-test case pins that silence produces no finding, so "extend it to red on silence" cannot happen by accident.Measured on this branch's tree, the gate reports 2 findings. Both name the two
packages/triggers/*manifests whoserepository.directorystill points atpackages/plugins/plugin-trigger-*, which is precisely what PR #15993 corrects. Those two manifests are that PR's fence and are deliberately untouched here.Two options were on the table for handling that, and this PR takes the first:
The census, re-measured on this branch (the card's numbers are days old)
package.jsonrepository.directoryThe card's "after #15478" column is a prediction of a landing that has not happened: the two mismatches are still on the default branch, so the "before" column is the live state. Reproduce with
node scripts/check-manifest-repository-directory.mjs --list.The trigger is "this manifest changed at all", not "this manifest moved"
The gate is a state check over the whole population on every run and reads no diff, which is the shape the corrected history asks for.
f15d6f6f6was a 26-file COPY that edited each manifest'snameand leftdirectoryon the old path — which still existed, so the field RESOLVED and pointed at another package.ea4941ad8was the pure rename.9a43e042fthen editedrepository.urlINSIDE THE SAME OBJECT with the staledirectoryline as visible hunk context, and shipped. A rule that fired only on directory moves would have missed the third entirely, exactly as the reviewers did. The dispatch derivation carries the same shape one level up: the declared population is the manifest file kind under every root that holds one, so a card that touches a manifest at all is told to run this gate.That history is also why OWN is a separate invariant from RESOLVES rather than folded into it. A resolves-only gate would have been GREEN during the copy window and would then have reddened on the unrelated commit that deleted the originals, naming a defect introduced somewhere else. The two are reported apart because they tell an author different things: STALE means the path is gone, MISPLACED means the link sends a consumer to somebody else's package.
The anti-vacuity anchor
The population is
git ls-filesplus a field predicate, so it empties silently from either side. Three instruments, and no verdict prints unless all of them are satisfied:truepasses an EXISTS-only control while silencing every finding.MEASUREDrecords manifests/declaring/trackedDirectories on a named commit, with floors under each and a provenance line printed on every pass so the record cannot silently stop describing the tree. A run below a floor REFUSES with its own exit code and never offers moving the floor as the repair.SELF_TEST_BATTERIESpins the battery NAMES and each battery's case floor, evaluated before the verdict, so a self-test that runs zero cases cannot print a success line.Proven red before its green was believed. Four ablations, each mutation confirmed on disk before the reading was taken and each restored from
HEADwithgit diff HEADempty and the blob hash equal to HEAD's:battery ... DID NOT RUN — 0 cases registered, 11 pinnedtrueEvery one of those would have been a clean green without the instrument that caught it.
And proven green, on the real population. The shipped
scanwas driven over the live tracked listing with only the two fenced manifests patched IN MEMORY, on disk untouched: 2 findings before, 0 after, both control probes fired, verdict census 57 ok / 24 undeclared.Verification
Gate family derived mechanically on the final head and run in full:
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands— 55 commands, 54 green, the one non-green being this PR's own new gate on the two fenced manifests described above. Exit codes captured immediately after a single redirect, never through a pipe.pnpm check:dts-closure,check:dual-build-cjs-loadsandcheck:type-check-debtfirst answered exit 3 PREREQUISITE NOT MET; they were re-run to real readings afterpnpm build(type-check-debt under an 8192 MB heap) and are green.pnpm lint(eslint . --no-inline-config, whole tree) is green, so no narrowing was needed and none is claimed.pnpm check:pm-dispatch-gates— 1534 cases, all pass. Union run at48fab7beb.Two derivation-visibility repairs came out of that run and are worth naming, because both were silent failures:
**/package.jsonis judged correctly by the derivation'shintCoversand is never SEEN:extractWatchHintsadmits a literal only if it starts with a word character, a dot or an@, so a leading glob is dropped at admission and the declaration extracts to zero hints. The four literals now declared extract all four, and the gate is MATCHED for a manifest card and silent forpackages/spec/src/index.ts.scripts/pm/bare-root-worklist.mjssweeps for, and it reddened that sweep's self-test until the value was derived from the population instead.Changeset
skip-changeset. The diff is a new repo-internal gate script, acheck:script in the private root manifest, and a workflow step. Nothing here is inside a publishable package and no package's published surface moves, so this PR declares no release of its own.Note on the branch name
The claim comment names
claude/issue-15991-repository-directory-gate. That branch was pushed first and its initial commit carried aRefscard-relation trailer, whichcheck:partof-closing-keywordRULE 2 makes a finding — and that gate's own header forbids repairing it by rewriting pushed history. The corrected history therefore went to this branch instead; the first branch has no PR and can be deleted.Generated by Claude Code